package org.osite.xml;

import java.lang.reflect.Constructor;
import java.util.HashMap;

import org.osite.util.Nullable;
import org.osite.util.Preconditions;
import org.osite.util.ResourceLoader;
import org.osite.xml.converter.BooleanConverter;
import org.osite.xml.converter.DoubleConverter;
import org.osite.xml.converter.EnumConverter;
import org.osite.xml.converter.FloatConverter;
import org.osite.xml.converter.IntegerConverter;
import org.osite.xml.converter.StringConverter;
import org.w3c.dom.Element;

public class XmlParser implements ConvertorProvider {
	private final HashMap<String, EntityHandler<?>> handlers = 
			new HashMap<String, EntityHandler<?>>();
	private final HashMap<Class<?>, AttributeConverter<?>> convertors = 
			new HashMap<Class<?>, AttributeConverter<?>>();

	public Object parse(Element element) throws XmlParseException {
		return parse(element, null);
	}

	public Object parse(Element element, @Nullable ResourceLoader loader)
			throws XmlParseException {
		ParseContext context = new ParseContext(this, loader);
		return context.parseEntity(element);
	}

	public XmlParser addEntityHandler(EntityHandler<?> handler)
			throws ParseSetupException {
		String name = Preconditions.checkNotNull(handler.getTagName());
		if (handlers.containsKey(name)) {
			throw new ParseSetupException("duplicate tag names: " + name);
		}
		handlers.put(name, handler);
		return this;
	}

	public EntityHandler<?> getEntityHandler(String component) {
		return handlers.get(Preconditions.checkNotNull(component));
	}

	@SuppressWarnings("unchecked")
	public <T> AttributeConverter<T> getAttributeConverter(Class<T> type)
			throws ParseSetupException {
		Class<?> current = type;
		while (current != null) {
			AttributeConverter<?> convertor = convertors.get(current);
			if (convertor != null) {
				return (AttributeConverter<T>) convertor;
			}
			current = current.getSuperclass();
		}
		return null;
	}

	public <T> XmlParser addAttributeConvertor(Class<?> type,
			AttributeConverter<T> convertor) throws ParseSetupException {
		Preconditions.checkNotNull(type);
		Preconditions.checkNotNull(convertor);
		if (convertors.containsKey(type)) {
			throw new ParseSetupException("a convertor for " + type
					+ " is already registered");
		}
		convertors.put(type, convertor);
		return this;
	}
	
    @SuppressWarnings("unchecked")
    public <T> XmlParser registerComponent(Class<T> type)
			throws ParseSetupException {
		XmlEntity entity = type.getAnnotation(XmlEntity.class);
		Class<? extends EntityHandler> handlerType = 
		        (entity == null) ? EntityHandler.class : entity.handler();
		addEntityHandler(handlerType.isInterface() ? newDefaultComponentHandler(type)
				: newHandlerInstance(handlerType));
		return this;
	}

	private <T extends EntityHandler<T>> T newHandlerInstance(Class<T> type)
			throws ParseSetupException {
		try {
			try {
				Constructor<T> ctor = type
						.getDeclaredConstructor(XmlParser.class);
				if (!ctor.isAccessible()) {
					ctor.setAccessible(true);
				}
				return ParseUtil.newInstance(ctor, this);
			} catch (NoSuchMethodException e) {
				return ParseUtil.newInstance(type);
			}
		} catch (PasreReflectionException e) {
			throw new ParseSetupException(e);
		}
	}

	private <T> EntityHandler<T> newDefaultComponentHandler(
			Class<T> type) throws ParseSetupException {
		return new DefaultEntityHandler<T>(type, this);
	}

	public XmlParser addDefaultAttributeConvertors()
			throws ParseSetupException {
		addAttributeConvertor(boolean.class, new BooleanConverter());
		addAttributeConvertor(Boolean.class, new BooleanConverter());
		addAttributeConvertor(double.class, new DoubleConverter());
		addAttributeConvertor(Double.class, new DoubleConverter());
		addAttributeConvertor(Enum.class, new EnumConverter());
		addAttributeConvertor(float.class, new FloatConverter());
		addAttributeConvertor(Float.class, new FloatConverter());
		addAttributeConvertor(int.class, new IntegerConverter());
		addAttributeConvertor(Integer.class, new IntegerConverter());
		addAttributeConvertor(String.class, new StringConverter());
		return this;
	}
}
